import {retreatFrontier} from "../line/highlight.js"
import {startWorker} from "../display/highlight_worker.js"
import {operation} from "../display/operations.js"
import {regChange, regLineChange} from "../display/view_tracking.js"
import {clipLine, clipPos, cmp, Pos} from "../line/pos.js"
import {sawReadOnlySpans} from "../line/saw_special_spans.js"
import {lineLength, removeReadOnlyRanges, stretchSpansOverChange, visualLine} from "../line/spans.js"
import {getBetween, getLine, lineNo} from "../line/utils_line.js"
import {estimateHeight} from "../measurement/position_measurement.js"
import {hasHandler, signal, signalCursorActivity} from "../util/event.js"
import {indexOf, lst, map, sel_dontScroll} from "../util/misc.js"
import {signalLater} from "../util/operation_group.js"

import {changeEnd, computeSelAfterChange} from "./change_measurement.js"
import {isWholeLineUpdate, linkedDocs, updateDoc} from "./document_data.js"
import {addChangeToHistory, historyChangeFromChange, mergeOldSpans, pushSelectionToHistory} from "./history.js"
import {Range, Selection} from "./selection.js"
import {setSelection, setSelectionNoUndo, skipAtomic} from "./selection_updates.js"

// UPDATING

// Allow "beforeChange" event handlers to influence a change
function filterChange(doc, change, update) {
    let obj = {
        canceled: false,
        from: change.from,
        to: change.to,
        text: change.text,
        origin: change.origin,
        cancel: () => obj.canceled = true
    }
    if (update) obj.update = (from, to, text, origin) => {
        if (from) obj.from = clipPos(doc, from)
        if (to) obj.to = clipPos(doc, to)
        if (text) obj.text = text
        if (origin !== undefined) obj.origin = origin
    }
    signal(doc, "beforeChange", doc, obj)
    if (doc.cm) signal(doc.cm, "beforeChange", doc.cm, obj)

    if (obj.canceled) {
        if (doc.cm) doc.cm.curOp.updateInput = 2
        return null
    }
    return {from: obj.from, to: obj.to, text: obj.text, origin: obj.origin}
}

// Apply a change to a document, and add it to the document's
// history, and propagating it to all linked documents.
export function makeChange(doc, change, ignoreReadOnly) {
    if (doc.cm) {
        if (!doc.cm.curOp) return operation(doc.cm, makeChange)(doc, change, ignoreReadOnly)
        if (doc.cm.state.suppressEdits) return
    }

    if (hasHandler(doc, "beforeChange") || doc.cm && hasHandler(doc.cm, "beforeChange")) {
        change = filterChange(doc, change, true)
        if (!change) return
    }

    // Possibly split or suppress the update based on the presence
    // of read-only spans in its range.
    let split = sawReadOnlySpans && !ignoreReadOnly && removeReadOnlyRanges(doc, change.from, change.to)
    if (split) {
        for (let i = split.length - 1; i >= 0; --i)
            makeChangeInner(doc, {
                from: split[i].from,
                to: split[i].to,
                text: i ? [""] : change.text,
                origin: change.origin
            })
    } else {
        makeChangeInner(doc, change)
    }
}

function makeChangeInner(doc, change) {
    if (change.text.length == 1 && change.text[0] == "" && cmp(change.from, change.to) == 0) return
    let selAfter = computeSelAfterChange(doc, change)
    addChangeToHistory(doc, change, selAfter, doc.cm ? doc.cm.curOp.id : NaN)

    makeChangeSingleDoc(doc, change, selAfter, stretchSpansOverChange(doc, change))
    let rebased = []

    linkedDocs(doc, (doc, sharedHist) => {
        if (!sharedHist && indexOf(rebased, doc.history) == -1) {
            rebaseHist(doc.history, change)
            rebased.push(doc.history)
        }
        makeChangeSingleDoc(doc, change, null, stretchSpansOverChange(doc, change))
    })
}

// Revert a change stored in a document's history.
export function makeChangeFromHistory(doc, type, allowSelectionOnly) {
    let suppress = doc.cm && doc.cm.state.suppressEdits
    if (suppress && !allowSelectionOnly) return

    let hist = doc.history, event, selAfter = doc.sel
    let source = type == "undo" ? hist.done : hist.undone, dest = type == "undo" ? hist.undone : hist.done

    // Verify that there is a useable event (so that ctrl-z won't
    // needlessly clear selection events)
    let i = 0
    for (; i < source.length; i++) {
        event = source[i]
        if (allowSelectionOnly ? event.ranges && !event.equals(doc.sel) : !event.ranges)
            break
    }
    if (i == source.length) return
    hist.lastOrigin = hist.lastSelOrigin = null

    for (; ;) {
        event = source.pop()
        if (event.ranges) {
            pushSelectionToHistory(event, dest)
            if (allowSelectionOnly && !event.equals(doc.sel)) {
                setSelection(doc, event, {clearRedo: false})
                return
            }
            selAfter = event
        } else if (suppress) {
            source.push(event)
            return
        } else break
    }

    // Build up a reverse change object to add to the opposite history
    // stack (redo when undoing, and vice versa).
    let antiChanges = []
    pushSelectionToHistory(selAfter, dest)
    dest.push({changes: antiChanges, generation: hist.generation})
    hist.generation = event.generation || ++hist.maxGeneration

    let filter = hasHandler(doc, "beforeChange") || doc.cm && hasHandler(doc.cm, "beforeChange")

    for (let i = event.changes.length - 1; i >= 0; --i) {
        let change = event.changes[i]
        change.origin = type
        if (filter && !filterChange(doc, change, false)) {
            source.length = 0
            return
        }

        antiChanges.push(historyChangeFromChange(doc, change))

        let after = i ? computeSelAfterChange(doc, change) : lst(source)
        makeChangeSingleDoc(doc, change, after, mergeOldSpans(doc, change))
        if (!i && doc.cm) doc.cm.scrollIntoView({from: change.from, to: changeEnd(change)})
        let rebased = []

        // Propagate to the linked documents
        linkedDocs(doc, (doc, sharedHist) => {
            if (!sharedHist && indexOf(rebased, doc.history) == -1) {
                rebaseHist(doc.history, change)
                rebased.push(doc.history)
            }
            makeChangeSingleDoc(doc, change, null, mergeOldSpans(doc, change))
        })
    }
}

// Sub-views need their line numbers shifted when text is added
// above or below them in the parent document.
function shiftDoc(doc, distance) {
    if (distance == 0) return
    doc.first += distance
    doc.sel = new Selection(map(doc.sel.ranges, range => new Range(
        Pos(range.anchor.line + distance, range.anchor.ch),
        Pos(range.head.line + distance, range.head.ch)
    )), doc.sel.primIndex)
    if (doc.cm) {
        regChange(doc.cm, doc.first, doc.first - distance, distance)
        for (let d = doc.cm.display, l = d.viewFrom; l < d.viewTo; l++)
            regLineChange(doc.cm, l, "gutter")
    }
}

// More lower-level change function, handling only a single document
// (not linked ones).
function makeChangeSingleDoc(doc, change, selAfter, spans) {
    if (doc.cm && !doc.cm.curOp)
        return operation(doc.cm, makeChangeSingleDoc)(doc, change, selAfter, spans)

    if (change.to.line < doc.first) {
        shiftDoc(doc, change.text.length - 1 - (change.to.line - change.from.line))
        return
    }
    if (change.from.line > doc.lastLine()) return

    // Clip the change to the size of this doc
    if (change.from.line < doc.first) {
        let shift = change.text.length - 1 - (doc.first - change.from.line)
        shiftDoc(doc, shift)
        change = {
            from: Pos(doc.first, 0), to: Pos(change.to.line + shift, change.to.ch),
            text: [lst(change.text)], origin: change.origin
        }
    }
    let last = doc.lastLine()
    if (change.to.line > last) {
        change = {
            from: change.from, to: Pos(last, getLine(doc, last).text.length),
            text: [change.text[0]], origin: change.origin
        }
    }

    change.removed = getBetween(doc, change.from, change.to)

    if (!selAfter) selAfter = computeSelAfterChange(doc, change)
    if (doc.cm) makeChangeSingleDocInEditor(doc.cm, change, spans)
    else updateDoc(doc, change, spans)
    setSelectionNoUndo(doc, selAfter, sel_dontScroll)

    if (doc.cantEdit && skipAtomic(doc, Pos(doc.firstLine(), 0)))
        doc.cantEdit = false
}

// Handle the interaction of a change to a document with the editor
// that this document is part of.
function makeChangeSingleDocInEditor(cm, change, spans) {
    let doc = cm.doc, display = cm.display, from = change.from, to = change.to

    let recomputeMaxLength = false, checkWidthStart = from.line
    if (!cm.options.lineWrapping) {
        checkWidthStart = lineNo(visualLine(getLine(doc, from.line)))
        doc.iter(checkWidthStart, to.line + 1, line => {
            if (line == display.maxLine) {
                recomputeMaxLength = true
                return true
            }
        })
    }

    if (doc.sel.contains(change.from, change.to) > -1)
        signalCursorActivity(cm)

    updateDoc(doc, change, spans, estimateHeight(cm))

    if (!cm.options.lineWrapping) {
        doc.iter(checkWidthStart, from.line + change.text.length, line => {
            let len = lineLength(line)
            if (len > display.maxLineLength) {
                display.maxLine = line
                display.maxLineLength = len
                display.maxLineChanged = true
                recomputeMaxLength = false
            }
        })
        if (recomputeMaxLength) cm.curOp.updateMaxLine = true
    }

    retreatFrontier(doc, from.line)
    startWorker(cm, 400)

    let lendiff = change.text.length - (to.line - from.line) - 1
    // Remember that these lines changed, for updating the display
    if (change.full)
        regChange(cm)
    else if (from.line == to.line && change.text.length == 1 && !isWholeLineUpdate(cm.doc, change))
        regLineChange(cm, from.line, "text")
    else
        regChange(cm, from.line, to.line + 1, lendiff)

    let changesHandler = hasHandler(cm, "changes"), changeHandler = hasHandler(cm, "change")
    if (changeHandler || changesHandler) {
        let obj = {
            from: from, to: to,
            text: change.text,
            removed: change.removed,
            origin: change.origin
        }
        if (changeHandler) signalLater(cm, "change", cm, obj)
        if (changesHandler) (cm.curOp.changeObjs || (cm.curOp.changeObjs = [])).push(obj)
    }
    cm.display.selForContextMenu = null
}

export function replaceRange(doc, code, from, to, origin) {
    if (!to) to = from
    if (cmp(to, from) < 0) [from, to] = [to, from]
    if (typeof code == "string") code = doc.splitLines(code)
    makeChange(doc, {from, to, text: code, origin})
}

// Rebasing/resetting history to deal with externally-sourced changes

function rebaseHistSelSingle(pos, from, to, diff) {
    if (to < pos.line) {
        pos.line += diff
    } else if (from < pos.line) {
        pos.line = from
        pos.ch = 0
    }
}

// Tries to rebase an array of history events given a change in the
// document. If the change touches the same lines as the event, the
// event, and everything 'behind' it, is discarded. If the change is
// before the event, the event's positions are updated. Uses a
// copy-on-write scheme for the positions, to avoid having to
// reallocate them all on every rebase, but also avoid problems with
// shared position objects being unsafely updated.
function rebaseHistArray(array, from, to, diff) {
    for (let i = 0; i < array.length; ++i) {
        let sub = array[i], ok = true
        if (sub.ranges) {
            if (!sub.copied) {
                sub = array[i] = sub.deepCopy();
                sub.copied = true
            }
            for (let j = 0; j < sub.ranges.length; j++) {
                rebaseHistSelSingle(sub.ranges[j].anchor, from, to, diff)
                rebaseHistSelSingle(sub.ranges[j].head, from, to, diff)
            }
            continue
        }
        for (let j = 0; j < sub.changes.length; ++j) {
            let cur = sub.changes[j]
            if (to < cur.from.line) {
                cur.from = Pos(cur.from.line + diff, cur.from.ch)
                cur.to = Pos(cur.to.line + diff, cur.to.ch)
            } else if (from <= cur.to.line) {
                ok = false
                break
            }
        }
        if (!ok) {
            array.splice(0, i + 1)
            i = 0
        }
    }
}

function rebaseHist(hist, change) {
    let from = change.from.line, to = change.to.line, diff = change.text.length - (to - from) - 1
    rebaseHistArray(hist.done, from, to, diff)
    rebaseHistArray(hist.undone, from, to, diff)
}

// Utility for applying a change to a line by handle or number,
// returning the number and optionally registering the line as
// changed.
export function changeLine(doc, handle, changeType, op) {
    let no = handle, line = handle
    if (typeof handle == "number") line = getLine(doc, clipLine(doc, handle))
    else no = lineNo(handle)
    if (no == null) return null
    if (op(line, no) && doc.cm) regLineChange(doc.cm, no, changeType)
    return line
}
